// TODO: CODE REVIEW & CLEANUP!
//
// PolygonGlow.cs
//
// Implements PolygonGlow and related types.
//

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using HomeUX.ClientControlSupport;

namespace HomeUX.Controls
{

[TemplatePart(Name = LayoutRootName, Type = typeof(Panel))]
public class PolygonGlow : Control, IControlHelperConnector
{
    const string LayoutRootName = "LayoutRoot";

    Panel _layoutRoot;

    public static readonly DependencyProperty PointsProperty =
        DependencyProperty.Register("Points", typeof(PointCollection), typeof(PolygonGlow),
        new PropertyMetadata(OnAnyPropertyChanged));
    public static readonly DependencyProperty GlowThicknessProperty =
        DependencyProperty.Register("GlowThickness", typeof(double), typeof(PolygonGlow),
        new PropertyMetadata(OnAnyPropertyChanged));
    public static readonly DependencyProperty GlowColorProperty =
        DependencyProperty.Register("GlowColor", typeof(Color), typeof(PolygonGlow),
        new PropertyMetadata(OnAnyPropertyChanged));

    public PointCollection Points
    {
        get
        {
            return (PointCollection)GetValue(PointsProperty);
        }
        set
        {
            SetValue(PointsProperty, value);
        }
    }

    public double GlowThickness
    {
        get
        {
            return (double)GetValue(GlowThicknessProperty);
        }
        set
        {
            SetValue(GlowThicknessProperty, value);
        }
    }

    public Color GlowColor
    {
        get
        {
            return (Color)GetValue(GlowColorProperty);
        }
        set
        {
            SetValue(GlowColorProperty, value);
        }
    }

    public PolygonGlow()
    {
        DefaultStyleKey = typeof(PolygonGlow);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        ControlHelper.GetRequiredTemplateChild(this, LayoutRootName, out _layoutRoot);
        PerformLayout();
    }

    void PerformLayout()
    {
        _layoutRoot.Children.Clear();

        double glowThickness = GlowThickness;
        Color glowColor = GlowColor;

        PointCollection vertices = Points;
        if ((vertices == null) || (vertices.Count < 3))
            return;

        // Algorithm:
        //
        // In this example, the polygon is the lower 6-sided figure.  We process each of the 6
        // edges; in the example below, we're currently processing the top edge.  For each edge,
        // we define pt1 and pt2 as the endpoints of that edge, pt0 as the previous vertex
        // (counterclockwise from pt1), and pt3 as the next vertex (clockwise from pt2).
        //   
		//      ptC ___________________ ptD  ___
		//         |                   |      |
		//         |  Clip Extension   |      |Glow Thickness
        //      ptA|___________________|ptB  _|_
        //          \                 /       |
        //           \Clip Quadrangle/        |Glow Thickness
        //            \_____________/        _|_
        //            /pt1       pt2\
        //           /               \ 
        //          /                 \
        //         /                   \
        //        /                     \
        //    pt0|        Polygon        |pt3
        //       |                       |
        //       |                       |
        //       |                       |
        //       |_______________________|
        //
        // For the line segment shown (pt1 to pt2), we'll create a Line whose thickness is
        // twice the desired GlowThickness -- that line will have round endcaps, centered on
        // pt1 and pt3.  However, we need to clip away the portion of the line that's inside
        // the polygon, and we also don't want the line endcaps to overlap.  So, for the line
        // segment above, we'll create a clipping quadrangle (ptA,pt1,pt2,ptB), where ptA is
        // calculated so that the line (pt1,ptA) divides the angle (pt0,pt1,pt2) in half.
		//
		// The clipping quadrangle (ptA,pt1,pt2,ptB) won't be sufficient in the case where the
		// angle (pt1,pt2,pt3) is less than 90 degrees.  To account for that, we extend the
		// clipping quadrangle in the direction perpendicular to the current line segment
		// (pt1,pt2) by an additional GlowThickness.  The resulting clipping region for this
		// line segment is (pt1,ptA,ptC,ptD,ptB,pt2).
        //

        // start with <pt*> referring to the polygon edge between vertices[vertices.Count - 2] and
        // vertices[vertices.Count - 1]
        Point pt0 = vertices[vertices.Count - 3];
        Point pt1 = vertices[vertices.Count - 2];
        Point pt2 = vertices[vertices.Count - 1];
        Point pt3 = vertices[0];

        // set <angle01>, the direction from <pt0> to <pt1>; set
        // <angle12> and <angle23> similarly
        double angle01 = GetAngle(pt0, pt1);
        double angle12 = GetAngle(pt1, pt2);
        double angle23 = GetAngle(pt2, pt3);

        // calculate <angle10>, the direction from <pt0> to <pt1>
        double angle10 = ReverseAngle(angle01);

        // calculate <angle1A>, the direction from <pt1> to where <ptA> will be, by splitting
        // <angle10> and <angle12>
        double angle1A = angle10 + NormalizeAngle(angle12 - angle10) / 2;

        // calculate <angle21>, the direction from <pt2> to <pt1>
        double angle21 = ReverseAngle(angle12);

        // calculate <angle2B>, the direction from <pt2> to where <ptB> will be, by splitting
        // <angle21> and <angle23>
        double angle2B = angle21 + NormalizeAngle(angle23 - angle21) / 2;

        // calculate <ptA> and <ptB>; the "+ 1" is account for roundoff error, to ensure that the
        // clipping region includes everything it should
        Point ptA = RadialToPoint(pt1, angle1A, glowThickness + 1);
        Point ptB = RadialToPoint(pt2, angle2B, glowThickness + 1);

        // loop once for each polygon edge
        for (int vertexIndex = 0; vertexIndex < vertices.Count; vertexIndex++)
        {
            // calculate <ptC> and <ptD> by extending from <ptA> and <ptB>, respectively,
            // outward away from this edge
            double perpendicular = angle12 - Math.PI / 2;
            Point ptC = RadialToPoint(ptA, perpendicular, glowThickness + 1);
            Point ptD = RadialToPoint(ptB, perpendicular, glowThickness + 1);

            // draw the line
            var line = new Line()
            {
                UseLayoutRounding = false,
                X1 = pt1.X,
                Y1 = pt1.Y,
                X2 = pt2.X,
                Y2 = pt2.Y,
                Stroke = new LinearGradientBrush()
                {
                    MappingMode = BrushMappingMode.Absolute,
                    StartPoint = pt1,
                    EndPoint = RadialToPoint(pt1, angle12 - Math.PI / 2, glowThickness),
                    GradientStops = new GradientStopCollection()
                    {
                        new GradientStop()
                        {
                            Offset = 0,
                            Color = glowColor
                        },
                        new GradientStop()
                        {
                            Offset = 0.25,
                            Color = Color.FromArgb(240, glowColor.R, glowColor.G, glowColor.B)
                        },
                        new GradientStop()
                        {
                            Offset = 0.75,
                            Color = Color.FromArgb(16, glowColor.R, glowColor.G, glowColor.B)
                        },
                        new GradientStop()
                        {
                            Offset = 1,
                            Color = Color.FromArgb(0, glowColor.R, glowColor.G, glowColor.B)
                        }
                    }
                },
                StrokeThickness = glowThickness * 2,
                StrokeStartLineCap = PenLineCap.Round,
                StrokeEndLineCap = PenLineCap.Round,
                Clip = new GeometryGroup()
                {
                    //FillRule = FillRule.EvenOdd,
                    Children = new GeometryCollection()
                    {
                        //new RectangleGeometry()
                        //{
                        //    Rect = new Rect(-9999,-9999,32767,32767)
                        //},
                        new PathGeometry()
                        {
                            Figures = new PathFigureCollection()
                            {
                                PointsToPathFigure(pt1, ptA, ptC, ptD, ptB, pt2)
                            }
                        }
                    }
                }
            };
            _layoutRoot.Children.Add(line);

            /*
            // for debugging purposes, draw the clipping region in blue
            var clip = new Polyline()
            {
                Stroke = new SolidColorBrush(Colors.Blue),
                StrokeThickness = 1,
                Points = new PointCollection()
                {
                    pt1, ptA, ptC, ptD, ptB, pt2, pt1
                }
            };
            _layoutRoot.Children.Add(clip);
            */

            // move <pt*> and <angle*> to the next edge; note that <ptC> and <ptD> will be
            // recalculated in the next iteration
            pt0 = pt1;
            pt1 = pt2;
            pt2 = pt3;
            pt3 = vertices[(vertexIndex + 1) % vertices.Count];
            angle01 = angle12;
            angle12 = angle23;
            angle23 = GetAngle(pt2, pt3);
            ptA = ptB;
            angle21 = ReverseAngle(angle12);
            angle2B = angle21 + NormalizeAngle(angle23 - angle21) / 2;
            ptB = RadialToPoint(pt2, angle2B, glowThickness);
        }
    }

    static void OnAnyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (PolygonGlow)d;
        if (target._layoutRoot != null) // i.e. if OnApplyTemplate called yet
            target.PerformLayout();
    }

    PathFigure PointsToPathFigure(params Point[] vertices)
    {
        var pathFigure = new PathFigure()
        {
            StartPoint = vertices[0]
        };
        for (int index = 1; index < vertices.Length; index++)
        {
            pathFigure.Segments.Add(new LineSegment()
            {
                Point = vertices[index]
            });
        }
        return pathFigure;
    }

    /// <summary>
    /// Shortens a line segment by a given amount at each end, and calculates the angle of the line.
    /// </summary>
    /// <param name="x1"></param>
    /// <param name="y1"></param>
    /// <param name="x2"></param>
    /// <param name="y2"></param>
    /// <param name="amount"></param>
    /// <returns>The clockwise angle, in radians, between a line from (0,0) to (1,0) and this line.</returns>
    static double ShortenLine(ref double x1, ref double y1, ref double x2, ref double y2, double amount)
    {
        double dx = x2 - x1;
        double dy = y2 - y1;
        double angle = Math.Atan2(dy, dx);
        double length = Math.Sqrt(dx * dx + dy * dy);

        double x = x1, y = y1;
        double cos = Math.Cos(angle);
        double sin = Math.Sin(angle);
        x1 = x + amount * cos;
        y1 = y + amount * sin;
        x2 = x + (length - amount) * cos;
        y2 = y + (length - amount) * sin;

        return angle;
    }

    // TODO: delete:
    static double GetAngle(double x1, double y1, double x2, double y2)
    {
        return Math.Atan2(y2 - y1, x2 - x1);
    }

    static double GetAngle(Point pt1, Point pt2)
    {
        return NormalizeAngle(Math.Atan2(pt2.Y - pt1.Y, pt2.X - pt1.X));
    }

    static double ReverseAngle(double angle)
    {
        return NormalizeAngle(angle - Math.PI);
    }

    static double NormalizeAngle(double angle)
    {
        if (angle < 0)
            angle += Math.PI * 2;
        return angle;
    }

    static Point RadialToPoint(Point origin, double angle, double radius)
    {
        return new Point(origin.X + radius * Math.Cos(angle), origin.Y + radius * Math.Sin(angle));
    }

    Control IControlHelperConnector.GetControl()
    {
        return this;
    }

    DependencyObject IControlHelperConnector.GetTemplateChild(string childName)
    {
        return GetTemplateChild(childName);
    }
}

}
